package com.freewater.cargotracker.infrastructure.persistence.mybatis.repository;

import com.freewater.cargotracker.domain.model.voyage.CarrierMovement;
import com.freewater.cargotracker.domain.model.voyage.Voyage;
import com.freewater.cargotracker.domain.model.voyage.VoyageNumber;
import com.freewater.cargotracker.domain.model.voyage.VoyageRepository;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.converter.CarrierMovementConverter;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.converter.VoyageConverter;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.CarrierMovementPO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.VoyagePO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.CarrierMovementMapper;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.VoyageHelperMapper;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.VoyageMapper;
import com.github.meixuesong.aggregatepersistence.Aggregate;
import com.github.meixuesong.aggregatepersistence.AggregateFactory;
import com.github.meixuesong.aggregatepersistence.ChangedEntity;
import com.github.meixuesong.aggregatepersistence.DataObjectUtils;
import org.springframework.stereotype.Repository;

import javax.persistence.OptimisticLockException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Repository
public class VoyageRepositoryImpl implements VoyageRepository {

    private final VoyageMapper voyageMapper;
    private final VoyageHelperMapper voyageHelperMapper;
    private final CarrierMovementMapper carrierMovementMapper;

    public VoyageRepositoryImpl(VoyageMapper voyageMapper,
                                VoyageHelperMapper voyageHelperMapper,
                                CarrierMovementMapper carrierMovementMapper) {
        this.voyageMapper = voyageMapper;
        this.voyageHelperMapper = voyageHelperMapper;
        this.carrierMovementMapper = carrierMovementMapper;
    }

    @Override
    public void save(Voyage voyage) {
        save(AggregateFactory.createAggregate(voyage));
    }

    @Override
    public void save(Aggregate<Voyage> voyageAggregate) {
        if (voyageAggregate.isNew()) {
            voyageAggregate.getRoot().setVersion(1);
            insertNewAggregate(voyageAggregate);
        } else {
            updateAggregateRoot(voyageAggregate);
            insertCarrierMovements(voyageAggregate);
            removeCarrierMovements(voyageAggregate);
            updateCarrierMovements(voyageAggregate);
        }
    }

    @Override
    public Voyage findById(Long id) {
        final VoyagePO voyagePO = this.voyageMapper.findById(id);
        if (voyagePO == null) {
            return null;
        }
        this.voyageHelperMapper.findVoyageInfo(voyagePO);
        return VoyageConverter.deserialize(voyagePO);
    }

    @Override
    public Aggregate<Voyage> findAggregateById(Long id) {
        final Voyage voyage = findById(id);
        if (voyage == null) {
            return null;
        }
        return AggregateFactory.createAggregate(voyage);
    }

    @Override
    public Voyage findByVoyageNumber(VoyageNumber voyageNumber) {
        final VoyagePO voyagePO = this.voyageMapper.findByVoyageNumber(voyageNumber.getIdString());
        if (voyagePO == null) {
            return null;
        }
        this.voyageHelperMapper.findVoyageInfo(voyagePO);
        return VoyageConverter.deserialize(voyagePO);
    }

    @Override
    public Aggregate<Voyage> findAggregateByVoyageNumber(VoyageNumber voyageNumber) {
        final Voyage voyage = findByVoyageNumber(voyageNumber);
        if (voyage == null) {
            return null;
        }
        return AggregateFactory.createAggregate(voyage);
    }

    @Override
    public List<Voyage> findAll() {
        throw new UnsupportedOperationException();
    }

    @Override
    public double findBookedCargoSize(Long voyageId) {
        final double bookedCargoSize = voyageMapper.findBookedCargoSize(voyageId);
        return bookedCargoSize;
    }

    private void insertNewAggregate(Aggregate<Voyage> voyageAggregate) {
        final Voyage voyage = voyageAggregate.getRoot();
        final VoyagePO voyagePO = VoyageConverter.serialize(voyage);
        this.voyageMapper.insert(voyagePO);
        final Long voyageId = voyagePO.getId();
        voyage.setId(voyageId);
        //
        final List<CarrierMovementPO> carrierMovements = voyagePO.getCarrierMovements();
        // 设置自动生成的voyageId
        carrierMovements.forEach(movement -> movement.setVoyageId(voyageId));
        if (!carrierMovements.isEmpty()) {
            this.carrierMovementMapper.insertAll(carrierMovements);
        }
    }

    private void updateAggregateRoot(Aggregate<Voyage> voyageAggregate) {
        final Voyage root = voyageAggregate.getRoot();
        final Voyage snapshot = voyageAggregate.getRootSnapshot();
        final Set<String> changedFields = DataObjectUtils.getChangedFields(snapshot, root);
        // 注意，这里不管是否改变都需要修改version，否则乐观锁会失效
        final VoyagePO voyagePO = VoyageConverter.serialize(root);
        final int rowCount = this.voyageMapper.updateByIdSelective(voyagePO, changedFields);
        if (rowCount != 1) {
            throw new OptimisticLockException("Concurrency update failed");
        }
    }

    private void insertCarrierMovements(Aggregate<Voyage> voyageAggregate) {
        final Long voyageId = voyageAggregate.getRoot().getId();
        final Collection<CarrierMovement> newCarrierMovements = voyageAggregate
                .findNewEntities(voyage -> voyage.getSchedule().getCarrierMovements(),
                        carrierMovement -> carrierMovement.getId() == null);
        insertCarrierMovements(voyageId, newCarrierMovements);
    }

    private void insertCarrierMovements(Long voyageId, Collection<CarrierMovement> carrierMovements) {
        if (carrierMovements.isEmpty()) {
            return;
        }
        final List<CarrierMovementPO> carrierMovementPOs = carrierMovements.stream()
                .map(carrierMovement -> CarrierMovementConverter.serialize(voyageId, carrierMovement))
                .collect(Collectors.toList());
        this.carrierMovementMapper.insertAll(carrierMovementPOs);
    }

    private void removeCarrierMovements(Aggregate<Voyage> voyageAggregate) {
        final Collection<CarrierMovement> removedCarrierMovements = voyageAggregate
                .findRemovedEntities(voyage -> voyage.getSchedule().getCarrierMovements(),
                        CarrierMovement::getId);
        if (removedCarrierMovements.isEmpty()) {
            return;
        }
        final Set<Long> ids = removedCarrierMovements.stream().map(s -> s.getId()).collect(Collectors.toSet());
        this.carrierMovementMapper.deleteByIds(ids);
    }

    private void updateCarrierMovements(Aggregate<Voyage> voyageAggregate) {
        final Long voyageId = voyageAggregate.getRoot().getId();
        final Collection<ChangedEntity<CarrierMovement>> changedEntities = voyageAggregate
                .findChangedEntitiesWithOldValues(voyage -> voyage.getSchedule().getCarrierMovements(),
                        CarrierMovement::getId);
        if (changedEntities.isEmpty()) {
            return;
        }
        for (ChangedEntity<CarrierMovement> changedEntity : changedEntities) {
            final CarrierMovement oldMovement = changedEntity.getOldEntity();
            final CarrierMovement newMovement = changedEntity.getNewEntity();
            final Set<String> changedFields = DataObjectUtils.getChangedFields(oldMovement, newMovement);
            final CarrierMovementPO carrierMovementPO = CarrierMovementConverter.serialize(voyageId, newMovement);
            this.carrierMovementMapper.updateByIdSelective(carrierMovementPO, changedFields);
        }
    }
}
